officeparser 7.6.2 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/generators/HtmlGenerator.js +53 -14
- package/dist/generators/MarkdownGenerator.js +111 -40
- package/dist/officeparser.browser.d.ts +99 -18
- package/dist/officeparser.browser.iife.js +187 -180
- package/dist/officeparser.browser.mjs +187 -180
- package/dist/officeparser.browser.slim.d.ts +99 -18
- package/dist/officeparser.browser.slim.iife.js +168 -161
- package/dist/officeparser.browser.slim.mjs +168 -161
- package/dist/parsers/HtmlParser.js +36 -3
- package/dist/parsers/MarkdownParser.js +73 -8
- package/dist/sbom.cdx.json +161 -93
- package/dist/types.d.ts +99 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -780,6 +780,8 @@ idempotent and `.md → AST → HTML → AST → .md` survives unchanged.
|
|
|
780
780
|
| Attribute lists | `{width=50% .centered}` | `ImageMetadata.width` / `.align`, `TableMetadata.align` |
|
|
781
781
|
| Citations | `[@smith2024]` | `TextMetadata.citationKey` |
|
|
782
782
|
| Wikilinks | `[[Page]]` / `[[Page\|Alias]]` | `TextMetadata.wikilink`, `.link`, `.linkType` |
|
|
783
|
+
| Highlight | `==text==` | `TextMetadata.backgroundColor` |
|
|
784
|
+
| Link/image titles | `[text](url "Title")` / `` | `TextMetadata.title` / `ImageMetadata.title` |
|
|
783
785
|
| Inline/block math | `$E=mc^2$` / `` $$...$$ `` | `type: 'code'`, `CodeMetadata.math` (`'inline' \| 'block'`) |
|
|
784
786
|
| Frontmatter arrays | `tags: [a, b]` or `tags: ["a","b"]` | Real array in `metadata.customProperties`/`nativeProperties` |
|
|
785
787
|
| MDX components (import-only) | `<Component prop="x">...</Component>` | Stripped; inner Markdown is kept. Never generated back. |
|
|
@@ -795,7 +797,8 @@ save→reload cycle:
|
|
|
795
797
|
| HTML attribute | AST field | Notes |
|
|
796
798
|
|---|---|---|
|
|
797
799
|
| `data-width` / `data-align` / inline `style="width:…"` on `<img>` | `ImageMetadata.width` / `.align` | |
|
|
798
|
-
| `data-align` on `<table>` | `TableMetadata.align` | |
|
|
800
|
+
| `data-align` on `<table>` | `TableMetadata.align` | Emitted/parsed as per-column GFM markers (`:---`, `:---:`, `---:`); alignment rides `CellMetadata.align` |
|
|
801
|
+
| `title` on `<a>` / `<img>` | `TextMetadata.title` / `ImageMetadata.title` | Survives both directions (`[text](url "Title")` in Markdown) |
|
|
799
802
|
| `colspan` / `rowspan` on `<td>`/`<th>` | `CellMetadata.colSpan` / `.rowSpan` | Previously dropped on HTML import — merged cells now survive a save→reload cycle |
|
|
800
803
|
| `<div data-youtube-video="ID">` / `<iframe src="...youtube.com...">` | `type: 'embed'` | |
|
|
801
804
|
| `<ul data-type="taskList">` / `<li data-checked>` | `ListMetadata.isTask` / `.checked` | |
|
|
@@ -1146,7 +1149,8 @@ Pass as `mdConfig` inside `GeneratorConfig`.
|
|
|
1146
1149
|
|
|
1147
1150
|
| Option | Type | Default | Description |
|
|
1148
1151
|
|--------|------|---------|-------------|
|
|
1149
|
-
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1152
|
+
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `cellLineBreaks`/`itemLineBreaks` (default on) join multi-line table-cell / multi-paragraph list-item content with `<br>` instead of a space. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1153
|
+
| `dialect` | `MarkdownDialectPreset \| MarkdownDialectConfig` | `'extended'` | Which native syntax to emit for constructs that differ across targets (GitHub/GitLab/Obsidian/Pandoc/CommonMark). Each capability is typed by the syntax it selects (e.g. `strikethrough: 'tilde'`, `highlight: 'equals'`, `admonitions: 'blockquote'`), with `'none'` to turn it off. See [Markdown Dialect Support](#markdown-dialect-support). The old `boolean` toggles and admonition flavour names (`'github'`/`'gitlab'`/`'pandoc'`) still work but are deprecated. |
|
|
1150
1154
|
|
|
1151
1155
|
### PdfGeneratorConfig
|
|
1152
1156
|
|
|
@@ -446,7 +446,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
446
446
|
*/
|
|
447
447
|
async processNodeArray(nodes) {
|
|
448
448
|
let html = '';
|
|
449
|
-
// Stack to track active lists
|
|
449
|
+
// Stack to track active lists. `liClose` is the currently-open item's deferred closing
|
|
450
|
+
// suffix (`</li>`, or `</div></li>` for a task item): a list item is rendered WITHOUT its
|
|
451
|
+
// close so a deeper list can land inside it (spec-valid `<li>a<ul>...</ul></li>` rather
|
|
452
|
+
// than the invalid `<li>a</li><ul>...</ul>` sibling shape). The close is emitted when a
|
|
453
|
+
// same-level sibling arrives, when the level is popped, or at the end.
|
|
450
454
|
const listStack = [];
|
|
451
455
|
const openListTag = (type, isTask) => {
|
|
452
456
|
if (isTask)
|
|
@@ -457,7 +461,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
457
461
|
const closeListsToLevel = (level) => {
|
|
458
462
|
while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
|
|
459
463
|
const list = listStack.pop();
|
|
460
|
-
html += closeListTag(list.type) + '\n\n';
|
|
464
|
+
html += list.liClose + closeListTag(list.type) + '\n\n';
|
|
461
465
|
}
|
|
462
466
|
};
|
|
463
467
|
for (const node of nodes) {
|
|
@@ -492,20 +496,32 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
492
496
|
closeListsToLevel(indentation);
|
|
493
497
|
// Handle current level
|
|
494
498
|
if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
|
|
495
|
-
|
|
496
|
-
|
|
499
|
+
const top = listStack[listStack.length - 1];
|
|
500
|
+
if (top.type !== type || top.isTask !== isTask) {
|
|
501
|
+
// Kind changed at the same level: close the open item and the old list,
|
|
502
|
+
// then open the replacement list.
|
|
497
503
|
const last = listStack.pop();
|
|
498
|
-
html += closeListTag(last.type) + '\n';
|
|
504
|
+
html += last.liClose + closeListTag(last.type) + '\n';
|
|
499
505
|
html += openListTag(type, isTask) + '\n';
|
|
500
|
-
listStack.push({ indentation, type, isTask });
|
|
506
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Sibling at the same level: close the previous item before this one opens.
|
|
510
|
+
html += top.liClose;
|
|
501
511
|
}
|
|
502
512
|
}
|
|
503
513
|
else {
|
|
504
|
-
//
|
|
514
|
+
// Deeper level (or the first list): open a nested list INSIDE the currently
|
|
515
|
+
// open item, leaving the parent <li>'s close pending on its stack frame.
|
|
505
516
|
html += openListTag(type, isTask) + '\n';
|
|
506
|
-
listStack.push({ indentation, type, isTask });
|
|
517
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
518
|
}
|
|
508
519
|
html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
520
|
+
// Defer this item's close so a nested list can land inside it. A string override is
|
|
521
|
+
// a complete replacement item that already carries its own close, so add none.
|
|
522
|
+
listStack[listStack.length - 1].liClose = (typeof override === 'string')
|
|
523
|
+
? ''
|
|
524
|
+
: (isTask ? '</div></li>' : '</li>');
|
|
509
525
|
}
|
|
510
526
|
else {
|
|
511
527
|
// Non-list node closes all active lists
|
|
@@ -712,7 +728,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
712
728
|
imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
|
|
713
729
|
}
|
|
714
730
|
const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
|
|
715
|
-
const
|
|
731
|
+
const imgTitle = meta?.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
732
|
+
const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${imgTitle}${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
|
|
716
733
|
const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
|
|
717
734
|
return `${extraAnchors}<div${idAttr}>${content}</div>`;
|
|
718
735
|
}
|
|
@@ -831,7 +848,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
831
848
|
}
|
|
832
849
|
const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
|
|
833
850
|
const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
|
|
834
|
-
|
|
851
|
+
// A `code` node is always block-level (inline code is a monospace text run, emitted
|
|
852
|
+
// as <code> by formatText). Wrap in <pre> whenever it carries a language or spans
|
|
853
|
+
// multiple lines; only a bare single-line, language-less code node stays a <span>.
|
|
854
|
+
// Previously a single-line block (e.g. a one-line ```js) emitted <span><code>, which
|
|
855
|
+
// re-imports as inline code and which strict CodeBlock parsers (only <pre><code>) miss.
|
|
856
|
+
if (meta?.language || (node.text && node.text.includes('\n'))) {
|
|
835
857
|
return `${extraAnchors}<pre${idAttr}${className}${mappedAttrs}${styleAttr}>${codeHtml}</pre>`;
|
|
836
858
|
}
|
|
837
859
|
else {
|
|
@@ -839,16 +861,19 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
839
861
|
}
|
|
840
862
|
}
|
|
841
863
|
case 'list': {
|
|
864
|
+
// The closing suffix (`</div></li>` for a task item, `</li>` otherwise) is emitted
|
|
865
|
+
// by processNodeArray's list stack, not here, so a nested list can be placed inside
|
|
866
|
+
// this item before it closes. See `listStack`/`liClose` there.
|
|
842
867
|
const meta = node.metadata;
|
|
843
868
|
if (meta?.isTask) {
|
|
844
869
|
const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
|
|
845
870
|
const checkedBool = meta.checked ? ' checked' : '';
|
|
846
|
-
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}
|
|
871
|
+
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}`;
|
|
847
872
|
}
|
|
848
873
|
const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
|
|
849
874
|
? ` value="${meta.itemIndex + 1}"`
|
|
850
875
|
: '';
|
|
851
|
-
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}
|
|
876
|
+
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}`;
|
|
852
877
|
}
|
|
853
878
|
case 'table': {
|
|
854
879
|
// Smart Table Header Detection
|
|
@@ -1166,6 +1191,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1166
1191
|
let result = this.escape(text);
|
|
1167
1192
|
const f = node.formatting;
|
|
1168
1193
|
if (this.config.includeFormatting && f) {
|
|
1194
|
+
// Inline code: a monospace run becomes `<code>`, not a `font-family: monospace` span, so
|
|
1195
|
+
// an editor keying on <code> sees it and it re-imports as inline code (HtmlParser maps
|
|
1196
|
+
// <code> back to a monospace run). Innermost, so bold/italic wrap it (`<b><code>…`).
|
|
1197
|
+
if (f.font === 'monospace')
|
|
1198
|
+
result = `<code>${result}</code>`;
|
|
1169
1199
|
// Inside an `<hN>`, the heading's own styling is authoritative. A run that also carries
|
|
1170
1200
|
// bold and a font size - the normal case for ODF, where a heading's paragraph style is
|
|
1171
1201
|
// inherited by its runs - would wrap the text in `<b>` the heading already implies and,
|
|
@@ -1220,7 +1250,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1220
1250
|
else if (meta?.link) {
|
|
1221
1251
|
const isInternal = meta.linkType !== 'external';
|
|
1222
1252
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
1223
|
-
|
|
1253
|
+
const linkTitle = meta.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
1254
|
+
result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${linkTitle}${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
1224
1255
|
}
|
|
1225
1256
|
}
|
|
1226
1257
|
if (meta?.abbreviationTitle) {
|
|
@@ -1250,6 +1281,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1250
1281
|
const meta = node.metadata;
|
|
1251
1282
|
if (meta.alignment)
|
|
1252
1283
|
pushSafe('text-align', meta.alignment);
|
|
1284
|
+
// A table cell's column alignment (GFM `:---`/`:---:`/`---:`) lives on
|
|
1285
|
+
// `CellMetadata.align`, not `alignment`. Emit it as `text-align` on the `<th>`/`<td>`
|
|
1286
|
+
// so `HtmlParser` reads it back and the pipe-table markers survive AST -> HTML -> AST.
|
|
1287
|
+
// An unaligned cell (no `align`) adds nothing, keeping its HTML byte-identical.
|
|
1288
|
+
if (node.type === 'cell' && meta.align)
|
|
1289
|
+
pushSafe('text-align', meta.align);
|
|
1253
1290
|
if (meta.backgroundColor)
|
|
1254
1291
|
pushSafe('background-color', meta.backgroundColor);
|
|
1255
1292
|
if (meta.verticalAlign)
|
|
@@ -1274,7 +1311,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1274
1311
|
pushSafe('background-color', f.backgroundColor);
|
|
1275
1312
|
if (f.size && !options.skipFontSize)
|
|
1276
1313
|
pushSafe('font-size', f.size);
|
|
1277
|
-
|
|
1314
|
+
// A monospace run is emitted as <code> by formatText, so it must not also become a
|
|
1315
|
+
// font-family style here (that was the old, non-semantic inline-code shape).
|
|
1316
|
+
if (f.font && f.font !== 'monospace') {
|
|
1278
1317
|
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
1279
1318
|
if (safeFont)
|
|
1280
1319
|
styles.push(`font-family: ${safeFont}, sans-serif`);
|
|
@@ -27,12 +27,12 @@ const foldLines = (value) => String(value ?? '').replace(/[\r\n]+/g, ' ');
|
|
|
27
27
|
* exactly (every feature on, GitHub-style admonitions) - the backward-compatibility anchor.
|
|
28
28
|
*/
|
|
29
29
|
const MARKDOWN_DIALECT_PRESETS = {
|
|
30
|
-
extended: { admonitions: '
|
|
31
|
-
github: { admonitions: '
|
|
32
|
-
gitlab: { admonitions: '
|
|
33
|
-
obsidian: { admonitions: '
|
|
34
|
-
pandoc: { admonitions: '
|
|
35
|
-
commonmark: { admonitions: 'none', definitionLists:
|
|
30
|
+
extended: { admonitions: 'blockquote', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
31
|
+
github: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
32
|
+
gitlab: { admonitions: 'fence', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
33
|
+
obsidian: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
34
|
+
pandoc: { admonitions: 'fence-attribute', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'none', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
35
|
+
commonmark: { admonitions: 'none', definitionLists: 'none', footnotes: 'none', citations: 'none', wikilinks: 'none', math: 'none', attributeLists: 'none', strikethrough: 'none', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'html' },
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Normalizes `MdGeneratorConfig.dialect` into a fully-resolved preset. A string names a preset
|
|
@@ -40,6 +40,30 @@ const MARKDOWN_DIALECT_PRESETS = {
|
|
|
40
40
|
* omitted field falls back to - NOT "whatever preset was ambient before", since config merging
|
|
41
41
|
* replaces the whole `dialect` field rather than layering an object on top of a prior string.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Coerces a per-capability field to its canonical syntax variant. An omitted value inherits `base`;
|
|
45
|
+
* a deprecated boolean maps `true` -> `onValue` and `false` -> `'none'` (the two are the only legacy
|
|
46
|
+
* inputs, dropped next major); an explicit syntax string passes through unchanged.
|
|
47
|
+
*/
|
|
48
|
+
function resolveToggle(value, onValue, base) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return base;
|
|
51
|
+
if (value === true)
|
|
52
|
+
return onValue;
|
|
53
|
+
if (value === false)
|
|
54
|
+
return 'none';
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
/** Maps the deprecated admonition flavor aliases to their syntax names; passes syntax names through. */
|
|
58
|
+
function resolveAdmonitions(value, base) {
|
|
59
|
+
switch (value) {
|
|
60
|
+
case undefined: return base;
|
|
61
|
+
case 'github': return 'blockquote';
|
|
62
|
+
case 'gitlab': return 'fence';
|
|
63
|
+
case 'pandoc': return 'fence-attribute';
|
|
64
|
+
default: return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
43
67
|
function resolveDialect(dialect) {
|
|
44
68
|
if (dialect === undefined)
|
|
45
69
|
return MARKDOWN_DIALECT_PRESETS.extended;
|
|
@@ -47,14 +71,15 @@ function resolveDialect(dialect) {
|
|
|
47
71
|
return MARKDOWN_DIALECT_PRESETS[dialect] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
48
72
|
const base = MARKDOWN_DIALECT_PRESETS[dialect.extends ?? 'extended'] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
49
73
|
return {
|
|
50
|
-
admonitions: dialect.admonitions
|
|
51
|
-
definitionLists: dialect.definitionLists
|
|
52
|
-
footnotes: dialect.footnotes
|
|
53
|
-
citations: dialect.citations
|
|
54
|
-
wikilinks: dialect.wikilinks
|
|
74
|
+
admonitions: resolveAdmonitions(dialect.admonitions, base.admonitions),
|
|
75
|
+
definitionLists: resolveToggle(dialect.definitionLists, 'colon', base.definitionLists),
|
|
76
|
+
footnotes: resolveToggle(dialect.footnotes, 'caret', base.footnotes),
|
|
77
|
+
citations: resolveToggle(dialect.citations, 'at', base.citations),
|
|
78
|
+
wikilinks: resolveToggle(dialect.wikilinks, 'double-bracket', base.wikilinks),
|
|
55
79
|
math: dialect.math ?? base.math,
|
|
56
|
-
attributeLists: dialect.attributeLists
|
|
57
|
-
strikethrough: dialect.strikethrough
|
|
80
|
+
attributeLists: resolveToggle(dialect.attributeLists, 'brace', base.attributeLists),
|
|
81
|
+
strikethrough: resolveToggle(dialect.strikethrough, 'tilde', base.strikethrough),
|
|
82
|
+
highlight: dialect.highlight ?? base.highlight,
|
|
58
83
|
bulletListMarker: dialect.bulletListMarker ?? base.bulletListMarker,
|
|
59
84
|
orderedListMarker: dialect.orderedListMarker ?? base.orderedListMarker,
|
|
60
85
|
emphasisMarker: dialect.emphasisMarker ?? base.emphasisMarker,
|
|
@@ -71,6 +96,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
71
96
|
// inlineFormatting is opt-in only: it is never enabled by the boolean form, since it changes
|
|
72
97
|
// default output. Every other field follows the boolean.
|
|
73
98
|
textFormatting: on, alignment: on, anchors: on, tables: on, embeds: on, cellLineBreaks: on,
|
|
99
|
+
itemLineBreaks: on,
|
|
74
100
|
inlineFormatting: false,
|
|
75
101
|
});
|
|
76
102
|
if (fallbackToHtml === undefined || typeof fallbackToHtml === 'boolean')
|
|
@@ -83,6 +109,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
83
109
|
tables: fallbackToHtml.tables ?? on.tables,
|
|
84
110
|
embeds: fallbackToHtml.embeds ?? on.embeds,
|
|
85
111
|
cellLineBreaks: fallbackToHtml.cellLineBreaks ?? on.cellLineBreaks,
|
|
112
|
+
itemLineBreaks: fallbackToHtml.itemLineBreaks ?? on.itemLineBreaks,
|
|
86
113
|
inlineFormatting: fallbackToHtml.inlineFormatting ?? on.inlineFormatting,
|
|
87
114
|
};
|
|
88
115
|
}
|
|
@@ -163,10 +190,13 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
163
190
|
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
164
191
|
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
165
192
|
*/
|
|
166
|
-
renderAttributeList(meta) {
|
|
167
|
-
if (
|
|
193
|
+
renderAttributeList(meta, options = {}) {
|
|
194
|
+
if (this.resolvedDialect.attributeLists === 'none')
|
|
195
|
+
return '';
|
|
196
|
+
if (!meta)
|
|
168
197
|
return '';
|
|
169
|
-
|
|
198
|
+
const align = options.skipAlign ? undefined : meta.align;
|
|
199
|
+
if (!meta.width && !align)
|
|
170
200
|
return '';
|
|
171
201
|
const parts = [];
|
|
172
202
|
// Allowlist, not escape. These land in `metadata.width`/`align` on reparse, which the
|
|
@@ -182,8 +212,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
182
212
|
if (meta.width && MD_LENGTH_PATTERN.test(String(meta.width).trim())) {
|
|
183
213
|
parts.push(`width=${String(meta.width).trim()}`);
|
|
184
214
|
}
|
|
185
|
-
if (
|
|
186
|
-
parts.push(`align=${String(
|
|
215
|
+
if (align && MD_ALIGN_VALUES.has(String(align).trim().toLowerCase())) {
|
|
216
|
+
parts.push(`align=${String(align).trim().toLowerCase()}`);
|
|
187
217
|
}
|
|
188
218
|
if (parts.length === 0)
|
|
189
219
|
return '';
|
|
@@ -283,8 +313,19 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
283
313
|
text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
|
|
284
314
|
if (node.formatting.italic)
|
|
285
315
|
text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
|
|
286
|
-
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough)
|
|
316
|
+
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough !== 'none')
|
|
287
317
|
text = `~~${text}~~`;
|
|
318
|
+
// `==text==` highlight, in dialects that define it (Obsidian/extended). A plain
|
|
319
|
+
// highlight (the default yellow) always becomes `==text==`; a highlight carrying
|
|
320
|
+
// a SPECIFIC colour stays a background-color <span> when `inlineFormatting` is on,
|
|
321
|
+
// so its exact colour survives. With `inlineFormatting` off (no span to hold it)
|
|
322
|
+
// even a coloured highlight degrades to `==` rather than being dropped. In
|
|
323
|
+
// GFM/CommonMark `==` is literal, so a highlight falls through to the <span> path.
|
|
324
|
+
const isDefaultHighlight = node.formatting.backgroundColor === '#ffff00';
|
|
325
|
+
const emitHighlightMark = !!node.formatting.backgroundColor && this.resolvedDialect.highlight !== 'none'
|
|
326
|
+
&& (isDefaultHighlight || !this.resolvedFallbackToHtml.inlineFormatting);
|
|
327
|
+
if (emitHighlightMark)
|
|
328
|
+
text = `==${text}==`;
|
|
288
329
|
// Use HTML tags for formatting not natively supported by standard Markdown
|
|
289
330
|
if (this.resolvedFallbackToHtml.textFormatting) {
|
|
290
331
|
if (node.formatting.underline)
|
|
@@ -307,14 +348,18 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
307
348
|
styles.push(`${prop}: ${safe}`);
|
|
308
349
|
};
|
|
309
350
|
pushStyle('color', node.formatting.color);
|
|
310
|
-
|
|
351
|
+
// Skip the background-color only when it was already emitted as `==text==`
|
|
352
|
+
// above; a specific-colour highlight in a highlight dialect still keeps its
|
|
353
|
+
// exact colour here.
|
|
354
|
+
if (!emitHighlightMark)
|
|
355
|
+
pushStyle('background-color', node.formatting.backgroundColor);
|
|
311
356
|
pushStyle('font-size', node.formatting.size);
|
|
312
357
|
if (styles.length)
|
|
313
358
|
text = `<span style="${styles.join('; ')}">${text}</span>`;
|
|
314
359
|
}
|
|
315
360
|
}
|
|
316
361
|
const meta = node.metadata;
|
|
317
|
-
if (meta?.wikilink && this.resolvedDialect.wikilinks) {
|
|
362
|
+
if (meta?.wikilink && this.resolvedDialect.wikilinks !== 'none') {
|
|
318
363
|
// Obsidian syntax: bare page name, or page|alias when the display
|
|
319
364
|
// text differs from the page name. Strip the `[]|`/newline chars
|
|
320
365
|
// that would break out of the `[[...]]` wrapper.
|
|
@@ -340,8 +385,11 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
340
385
|
link = '#' + this.slugify(target);
|
|
341
386
|
}
|
|
342
387
|
// Reject javascript:/data: schemes and encode `()`/whitespace so the
|
|
343
|
-
// URL can't break out of `](...)` or inject a script link.
|
|
344
|
-
|
|
388
|
+
// URL can't break out of `](...)` or inject a script link. An advisory
|
|
389
|
+
// title follows as `"title"` (quotes inside it escaped), matching what
|
|
390
|
+
// the parser reads back.
|
|
391
|
+
const linkTitle = meta.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
392
|
+
text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)}${linkTitle})`;
|
|
345
393
|
}
|
|
346
394
|
}
|
|
347
395
|
if (meta?.abbreviationTitle) {
|
|
@@ -358,7 +406,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
358
406
|
// this branch also replaces `text` wholesale, so a strip that left `<`
|
|
359
407
|
// behind discarded the escaping applied earlier.
|
|
360
408
|
const key = String(meta.citationKey).replace(/[^a-zA-Z0-9_:.-]/g, '');
|
|
361
|
-
text = this.resolvedDialect.citations ? `[@${key}]` : `[${key}]`;
|
|
409
|
+
text = this.resolvedDialect.citations !== 'none' ? `[@${key}]` : `[${key}]`;
|
|
362
410
|
}
|
|
363
411
|
return text;
|
|
364
412
|
}
|
|
@@ -408,7 +456,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
408
456
|
? (meta.checked ? `${bullet}[x] ` : `${bullet}[ ] `)
|
|
409
457
|
: (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}${this.resolvedDialect.orderedListMarker} ` : bullet);
|
|
410
458
|
const anchors = this.renderAnchors(meta);
|
|
411
|
-
|
|
459
|
+
// A list item is a single Markdown line. HTML-origin items carry `paragraph`
|
|
460
|
+
// children (e.g. `<li><p>a</p><ul>...`), whose renderer appends `\n\n`; dumped
|
|
461
|
+
// verbatim that produces `- a\n\n\n - a1`, whose blank line splits the list
|
|
462
|
+
// apart and whose indent is then stripped on reparse, flattening the nesting.
|
|
463
|
+
// Collapse the item's internal breaks the same way table cells do (see the
|
|
464
|
+
// `cellLineBreaks` handling in renderMarkdownTable): join with `<br>` when the
|
|
465
|
+
// fallback is on, a space when off. Block children (code fences, tables) inside
|
|
466
|
+
// an item degrade under this join, exactly as they do inside a cell.
|
|
467
|
+
const br = this.resolvedFallbackToHtml.itemLineBreaks ? '<br>' : ' ';
|
|
468
|
+
const content = childrenOutput.trim().replace(/[ \t]*\n+/g, br);
|
|
469
|
+
return `${indent}${marker}${anchors}${content}\n`;
|
|
412
470
|
}
|
|
413
471
|
case 'image': {
|
|
414
472
|
if (!this.config.includeImages)
|
|
@@ -427,7 +485,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
427
485
|
// Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
|
|
428
486
|
const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
|
|
429
487
|
const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
|
|
430
|
-
|
|
488
|
+
const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
489
|
+
return `${anchors}${anchors ? '\n' : ''}${this.renderAttributeList(meta)}`;
|
|
431
490
|
}
|
|
432
491
|
case 'table': {
|
|
433
492
|
const anchors = this.renderAnchors(node.metadata);
|
|
@@ -437,7 +496,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
437
496
|
// only the plain pipe-table form needs the attribute-list syntax for alignment.
|
|
438
497
|
const usedHtmlFallback = this.resolvedDialect.tables === 'html' ||
|
|
439
498
|
(this.resolvedFallbackToHtml.tables && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node)));
|
|
440
|
-
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata);
|
|
499
|
+
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata, { skipAlign: true });
|
|
441
500
|
if (attrList) {
|
|
442
501
|
// Must glue directly below the last row with no blank line, or
|
|
443
502
|
// MarkdownParser's block splitter won't see it as part of the same block.
|
|
@@ -530,7 +589,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
530
589
|
case 'note': {
|
|
531
590
|
const meta = node.metadata;
|
|
532
591
|
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
533
|
-
if (
|
|
592
|
+
if (this.resolvedDialect.footnotes === 'none') {
|
|
534
593
|
// Dialect has no footnote syntax - the caller inlines this bare body
|
|
535
594
|
// as a parenthetical at the reference point instead of collecting it
|
|
536
595
|
// into an end-of-document "### Notes" section under a [^id] marker.
|
|
@@ -586,12 +645,12 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
586
645
|
const title = meta?.title ? (0, sanitize_js_1.markdownEscapeText)(foldLines(meta.title)) : '';
|
|
587
646
|
const body = childrenOutput.trim();
|
|
588
647
|
switch (this.resolvedDialect.admonitions) {
|
|
589
|
-
case '
|
|
648
|
+
case 'fence':
|
|
590
649
|
// GLFM fenced-div: no dedicated title syntax, so a custom title (if
|
|
591
650
|
// any) is folded into the body as a bold first line.
|
|
592
651
|
return `:::${type}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
593
|
-
case '
|
|
594
|
-
// Pandoc's own fenced-div-with-class syntax; same title handling as
|
|
652
|
+
case 'fence-attribute':
|
|
653
|
+
// Pandoc's own fenced-div-with-class syntax; same title handling as fence.
|
|
595
654
|
return `::: {.${type}}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
596
655
|
case 'none': {
|
|
597
656
|
// Degrade to a plain bold-labeled blockquote, no special marker.
|
|
@@ -599,7 +658,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
599
658
|
const heading = title || label.charAt(0) + label.slice(1).toLowerCase();
|
|
600
659
|
return `> **${heading}:**\n${quotedLines}\n\n`;
|
|
601
660
|
}
|
|
602
|
-
case '
|
|
661
|
+
case 'blockquote':
|
|
603
662
|
default: {
|
|
604
663
|
// Canonical GitHub blockquote form. No dedicated title syntax either
|
|
605
664
|
// (matches this library's historical output).
|
|
@@ -609,15 +668,15 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
609
668
|
}
|
|
610
669
|
}
|
|
611
670
|
case 'definitionList':
|
|
612
|
-
if (
|
|
671
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
613
672
|
return `${childrenOutput}\n`;
|
|
614
673
|
return `${childrenOutput}\n`;
|
|
615
674
|
case 'definitionTerm':
|
|
616
|
-
if (
|
|
675
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
617
676
|
return `**${childrenOutput}**\n\n`;
|
|
618
677
|
return `${childrenOutput}\n`;
|
|
619
678
|
case 'definitionDescription':
|
|
620
|
-
if (
|
|
679
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
621
680
|
return `${childrenOutput}\n\n`;
|
|
622
681
|
return `: ${childrenOutput}\n`;
|
|
623
682
|
case 'chart':
|
|
@@ -732,7 +791,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
732
791
|
// end-of-document "### Notes" section, or its content would be duplicated.
|
|
733
792
|
const isInlinedFootnote = (note) => {
|
|
734
793
|
const meta = note.metadata;
|
|
735
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
794
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
736
795
|
};
|
|
737
796
|
this.collectNotesFrom(node);
|
|
738
797
|
let result = await processor(node, childrenOutput);
|
|
@@ -848,7 +907,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
848
907
|
return;
|
|
849
908
|
const isInlinedFootnote = (note) => {
|
|
850
909
|
const meta = note.metadata;
|
|
851
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
910
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
852
911
|
};
|
|
853
912
|
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
854
913
|
}
|
|
@@ -892,7 +951,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
892
951
|
let cellContent = await this.processNodeRecursive(cellNode, processor);
|
|
893
952
|
// Use <br> fallback only if allowed, otherwise space
|
|
894
953
|
const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
|
|
895
|
-
|
|
954
|
+
// Consume any trailing spaces before the newline(s) too, so a hard-break's
|
|
955
|
+
// ` \n` collapses to a single `<br>` instead of leaving ` <br>` in the cell.
|
|
956
|
+
cellContent = cellContent.trim().replace(/[ \t]*\n+/g, br).replace(/\|/g, '\\|');
|
|
896
957
|
rowCells.push(cellContent);
|
|
897
958
|
// Handle colspan by adding empty cells
|
|
898
959
|
const colSpan = cellNode.metadata?.colSpan || 1;
|
|
@@ -909,7 +970,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
909
970
|
this.inImplicitBold = wasInImplicitBold;
|
|
910
971
|
}
|
|
911
972
|
}
|
|
912
|
-
// Second pass: Build table string with separator
|
|
973
|
+
// Second pass: Build table string with separator. The separator carries standard GFM
|
|
974
|
+
// per-column alignment (`:---`/`:---:`/`---:`) from columnAlignments, or the single table-level
|
|
975
|
+
// align applied to every column, rather than a non-standard trailing `{align}` attribute list.
|
|
976
|
+
const tableMeta = node.metadata;
|
|
977
|
+
// Column alignment lives on each cell (CellMetadata.align); read it off the header row.
|
|
978
|
+
// Fall back to the single table-level align (an editor's data-align) for every column.
|
|
979
|
+
const headerCells = (node.children?.[0]?.children || []).filter(c => c.type === 'cell');
|
|
980
|
+
const alignMarker = (i) => {
|
|
981
|
+
const a = headerCells[i]?.metadata?.align ?? tableMeta?.align;
|
|
982
|
+
return a === 'center' ? ':---:' : a === 'left' ? ':---' : a === 'right' ? '---:' : '---';
|
|
983
|
+
};
|
|
913
984
|
for (let i = 0; i < processedRows.length; i++) {
|
|
914
985
|
const row = processedRows[i];
|
|
915
986
|
// Pad row with empty cells if it has fewer than maxCols
|
|
@@ -918,7 +989,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
918
989
|
tableOutput += `| ${row.join(' | ')} |\n`;
|
|
919
990
|
if (i === 0) {
|
|
920
991
|
// Header separator
|
|
921
|
-
tableOutput += `| ${Array(maxCols
|
|
992
|
+
tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
|
|
922
993
|
}
|
|
923
994
|
}
|
|
924
995
|
return `\n${tableOutput}\n`;
|